Skip to main content

CSS 的绝对单位、相对单位、最小单位

移动端适配的几大问题:

绝对单位问题,10px 的线在不同 dpr 屏幕上的视觉效果一致。

相对单位问题,元素的大小随屏幕的宽高缩放。

最小单位问题,1px的线。

绝对单位与相对单位问题#

产生原因#

dpr(设备像素比) = 物理像素(设备像素) / 设备独立像素(css像素)

普通屏幕的 dpr=1,高清屏幕的 dpr>=2。我们在样式代码上定义的元素大小是 CSS 像素,这意味着在普通屏和高清屏上展现的像素会有不同,同时也伴随这图片高清问题。例如,width: 2px; height: 2px 在普通屏上物理像素是 2X2=4,在高清屏上则是(2X2)X(2X2)=16。

从设计稿得到的尺寸通常是以 px 为单位的,如果要换算成 rem 作单位需要依赖编译器插件,而且会出现小数的情况,一般的做法是:

  1. 自定义 flexible.js 文件,将计算 font-size 标准值的 n 定为 3.75,这样可以使得在高清屏中 1rem = 100px。

  2. 在 Webpack 里使用 px2rem-loader。

flexible 1.0#

原理:根据不同设备的 dpr 值和宽度, 为根元素 html 动态设置 font-size,其他元素使用 rem(相对于根元素的 font-size 标准值的大小)做单位,并且监听 resize 事件重新计算根元素的 font-size。

根元素 font-size = document.documentElement.clientWidth * dpr / n

设备宽高#

  1. 与 window 相关的宽高

    window.innerWidth 获得的是可视区域的宽,包含了纵向滚动条的宽度(IE8以及低版本浏览器不支持)。

    window.outerWidth 获得的是整个浏览器窗口的宽,包括侧边栏(如果存在)、窗口镶边(window chrome)和调正窗口大小的边框。包含调试窗及浏览器边框

    window.screen.width 获得的是整个屏幕的宽。

    window.screen.availWidth 获得的是浏览器窗口可使用的宽。

  2. 与 client 相关的宽高

    指的是元素的可视部分宽度和高度,即 padding + content 如果没有滚动条,即为元素设定的高度和宽度。 如果出现滚动条,滚动条会遮盖元素的宽高,那么该属性就是其本来的宽高减去滚动条的宽高。

    document.documentElement.clientWidth 获得的是屏幕可视区域的宽,不包括滚动条与工具条。

    document.body.clientWidth 获得的也是可视区域的宽度,但是document.body.clientHeight 获得的是body内容的高度,如果内容只有 200px,那么这个高度也是200px,如果想通过它得到屏幕可视区域的宽高,需要样式设置.

  3. 与 offset 相关的宽高

    指的是元素的 border + padding + content的宽度和高度。 该属性和其内部的内容是否超出元素大小无关,只和本来设定的 border 以及 width 和 height 有关。

  4. 与 scroll 相关的宽高

    JavaScript中的各种宽高属性

flexible 2.0#

https://github.com/amfe/lib-flexible

由于viewport单位得到众多浏览器的兼容,vw、vh、vmin、vmax,100vw 表示布局视口的宽度,100vh 表示布局视口的高度,与 计算 html 的 font-size 标准值 rem 有异曲同工之妙,但写法更简洁。

浏览器支持情况如下:can i use

最小单位问题#

产生原因#

对于高清屏幕来说,设置CSS像素1px大小,其实横跨的是dpr个设备像素,这样看起来线条不够细,与设计稿产生出入。当我们需要在屏幕上实现1px时,实际上写的应该是0.5px / dpr,但是目前只有 iOS 8+ 系统支持,安卓系统不支持。

因此,检测当前设备是否支持0.5px,如果不支持那么测试元素的高度会小于1px;如果支持则添加hairlines类样式。

解决方案#

  1. 使用 border-image

    但是 border 颜色变了就得重新制作图片,而且圆角会比较模糊。

  2. 使用 viewport 的 scale 缩放

    适用于新项目,老项目的维护改动较大。

  3. 使用伪元素和 transform

    伪元素设置绝对定位,并且和父元素左上角对其。将伪元素的长和宽先放大2倍,然后再设置一个边框,以左上角为中心,缩放到原来的0.5倍

    优点:全机型兼容,实现了真正的1px,而且可以圆角。

    缺点:暂用了伪元素,可能影响清除浮动。

刘海屏#

市面上的刘海屏和全面屏方案五花八门,如小米开发者文档提供的图所示:

![safearea](/img/blog/2020-05-17-CSS 的绝对单位、相对单位、最小单位/safearea.png)

上述两种屏幕都可以统称为刘海屏,不过对于右侧较小的刘海,业界一般称为水滴屏或美人尖。为便于说明,一般提到的「刘海屏」「刘海区」都同时指代上图两种屏幕。

当开发者在谈刘海屏适配问题时,主要是在谈论:如何避免内容被刘海和下巴遮挡。

通用的处理逻辑是:

  1. 判断设备的厂商和型号,是否是刘海屏。

  2. 如果设备是刘海屏,通过接口获取刘海区的宽高(刘海的左上角和右下角的坐标)。

对于安卓设备:

  1. Android O ,各家厂商有自己的实现方案。

    华为、小米、oppo,这三家都给了完整的解决方案。

  2. Android P(9.0)开始,官方提供了适配异形屏的方式 Support display cutouts。

    通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。 使用 getDisplayCutout() 函数可以确定这些凹口屏幕区域是否存在及其位置。

    窗口布局属性 layoutInDisplayCutoutMode 控制内容如何呈现在刘海区域中:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 这是默认行为,在竖屏模式下,内容会呈现到刘海区域中;但在横屏模式下,内容会显示黑边。

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 在竖屏模式和横屏模式下,内容都会呈现到刘海区域中。

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 内容从不呈现到刘海区域中。

对于苹果设备:

  1. iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length),这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

  2. iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念,官方建议: 不要把 Control 放在 Safe Area 之外的地方。

总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(在 iPhoneX上才有值)。 在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。

/* 兼容 UIWebview */
.safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top)
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom)
}
/* iphoneX 底部 Home Indicator高度 */
.iphone .safe-area-bottom-2X {
padding-bottom: calc(constant(safe-area-inset-bottom) / 2);
padding-bottom: calc(env(safe-area-inset-bottom) / 2);
}

要考虑兼容“有 native header 的页面和无 native header 的页面相互跳转”的场景, 当有native header时,safe area 值为 0。

参考:

Android刘海屏、水滴屏全面屏适配方案 最近很火的 Safe Area 到底是什么